iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
JavaScript

30天的 JavaScript 設計模式之旅系列 第 29

[Day 29] Incremental Static Generation/Regeneration(ISR) 與渲染模式總結

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20241013/20168201fjCjWQcU3A.png

延續昨天介紹的 SSG,今天要介紹的是 SSG 的優化版本 Incremental Static Generation/Regeneration(ISR)。
為何會有 ISR 的出現?如同前篇所說,因為 SSG 需要預先渲染好所有路由需要的 HTML 檔案,當 HTML 變多時就會難以維護,且要更新內容時也需要全部重新建構,耗費成本較高,因此才出現 Incremental Static Generation(或稱 Regeneration,因為過程中有產生也有重新產生),此渲染模式中文稱作增量靜態產生/重新產生,也有人稱它為 iSSG(以下文章會以 ISR 稱呼)。

ISR 可算是一種 SSG 和 SSR 的混合體,它只預渲染某些靜態頁面,在使用者請求時,再依照需求渲染動態頁面,因此不會一開始就預渲染好全部路由的頁面,避免 SSG 一次預渲染大量頁面所耗費的成本,也節省修正或新增頁面內容就要全部重新 build 的時間與心力。

ISR 運作方式

ISR 以 2 種方式,在現有靜態網站建構後逐步導入更新:

1. 允許增加新頁面

運用 lazy loading 的概念,新頁面在第一次請求時才立即產生,產生過程中會先向使用者顯示 fallback 或 loading 的提示 UI,和之前的 SSG 相比。SSG 如果請求不存在的路由頁面,會直接顯示 404 的 fallback 而不是顯示 loading fallback。

Next.js 範例:

// pages/articles/[id].js

// 1. 在 getStaticPaths() 中,回傳希望 build 階段預先渲染的 id 列表,可先從資料庫取得所有文章,並生成一個包含所有文章 ID 的 paths 陣列
export async function getStaticPaths() {
  const articles = await getArticlesFromDatabase();

  const paths = articles.map((article) => ({
     params: { id: article.id }
  }));

  // fallback: true 代表找不到該 id 路徑的頁面時,不會直接顯示 404,而是顯示 fallback 頁面
  // 這點和前面一般的 SSG 設定的 fallback false 不同
  return { paths, fallback: true };
}

// 2. params 會包含生成文章頁需要的 id
export async function getStaticProps({ params }) {
  return {
    props: {
      article: await getArticleFromDatabase(params.id)
    }
  }
}

export default function Article({ article }) {
  const router = useRouter();

  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  // 渲染文章頁面
}

使用者在請求頁面時,體驗上是差不多的,只有第一個請求還沒預渲染新頁面的使用者體驗會比較差(需要等待渲染好),後續請求的使用者都可以拿到預渲染好的頁面。預渲染和第一次請求時才渲染的示意圖如下。
https://ithelp.ithome.com.tw/upload/images/20241013/20168201sKaB1ne7r9.jpg
圖 1 預渲染好的頁面直接回傳(資料來源:自行繪製)

https://ithelp.ithome.com.tw/upload/images/20241013/20168201IsKGYGn7Tw.jpg
圖 2 請求時立即產生該頁面(資料來源:自行繪製)

2. 更新現有頁面

為現有頁面定義失效時間,只要超過失效時間,頁面就會重新驗證、重新產生新頁面。
此處使用的是 stale-while revalidate 策略,意思是在重新驗證頁面期間,使用者會先收到快取或舊的版本,重新驗證(重新產生)好以後,使用者下次請求就會收到新版本頁面,且重新驗證完全在背景執行,不需完全重建(rebuild)。

Next.js 範例:

// pages/articles/[id].js

// 1. getStaticProps 會在 build 階段執行
export async function getStaticProps() {
  return {
    props: {
      articles: await getArticlesFromDatabase(),
      revalidate: 60, // 回傳的 props 中,透過 revalidate 來強制頁面在 60 秒後重新驗證
    }
  }
}

// 2. page 元件會收到 getStaticProps 在 build 階段回傳的 articles prop
export default function Articles({ articles }) {
  return (
    <>
      <h1>Articles</h1>
      <ul>
        {articles.map((article) => (
          <li key={article.id}>{article.title}</li>
        ))}
      </ul>
    </>
  )
}

流程示意圖如下。
https://ithelp.ithome.com.tw/upload/images/20241013/20168201LZAsIrFvBZ.jpg
圖 3 更新現有頁面流程示意圖(資料來源:自行繪製)

補充:stale-while-revalidate
源自於 HTTP Cache-Control header 的一個屬性。
主要概念為:當第一次發出 request 時,瀏覽器會將回傳的資料存到快取裡,當之後又有相同的 request 時,瀏覽器會優先返回快取的版本,讓使用者可以迅速得到資料或是看到畫面,優化了使用者的體驗,並在 background 驗證快取的資料是不是已經過期,如果需要更新就會抓取最新資料並更新快取,當下次又有請求時就可以拿到剛剛更新過的、存到快取的資料。

優點

  • 支援動態資料:這是 ISR 概念會被提出來的原因之一,它能支援動態資料,而不需重新建構整個網站
  • 效能:幾乎與傳統 SSG 一樣快,因為重建頁面和拉取資料都在背景執行,不會影響太多客戶端或伺服器端的執行時間,另外還可減少傳統 SSG 一次建構大量頁面所花的建構時間
  • 可用性:頁面的新版本可維持在線上讓使用者訪問,即使新頁面在重新生成過程中失敗,也不影響舊版本,使用者還是可看到舊版本頁面
  • 一致性/穩定性:因為重新生成是在伺服器端逐頁進行,且不像傳統 SSG 要一次重建所有頁面,因此對資料庫和後端的負擔較小,能維持較好的效能穩定性
  • 分發便利性:和 SSG 相同,ISR 網站也可透過 CDN 快取,提供預渲染的網頁

缺點

  • 快取失效時間過短,可能有不必要的頁面再生成:頁面內容可能不像開發者定義的時間間隔那樣頻繁更新,這可能導致不必要的頁面再生成和快取失效,盡而導致更高的伺服器成本
  • 快取失效時間過長,可能會讓使用者看到較舊的資訊:頁面內容太久才失效、重新產生,可能會讓使用者看到較舊的、沒有即時更新的資訊
  • 和 SSG 相同,一樣需要伺服器成本來重新建置頁面並存放渲染好的頁面

按需求 ISR(On-demand Incremental Static Regeneration)

屬於 ISR 的一種變體,只會在某些特定事件發生時才重新產生頁面,而不是固定時間間隔重新產生。
常規(一般) ISR 的更新後頁面只會在已處理使用者頁面請求的邊緣節點被快取,而按需求 ISR 則是透過邊緣網路重新產生、分發頁面,全球使用者都可自動從邊緣快取看到頁面的最新版本。

按需求 ISR 和一般 ISR 相比,可避免不必要的重新產生和 serveless function 的呼叫,能降低營運成本,並提供更好的效能和開發者體驗(DX)。
而按需求 ISR 缺點則是靜態渲染大都會有的缺點,它不適合高度動態、具個人化資料的頁面。

渲染模式總結

這幾天介紹了 CSR、SSR、SSG 和 ISR 的渲染方式,實作上 Next.js 都有方法可以實現,因此使用 Next.js 開發時,可依據需求來為不同頁面選擇適合的渲染方式,以下附上四種渲染方式的流程比較:
image
圖 4 Next.js 支援四種渲染方式(資料來源:https://guydumais.digital/blog/next-js-the-ultimate-cheat-sheet-to-page-rendering/)

也附上圖表統整不同渲染模式的特色與使用案例。
(圖中的 progressive hydration 可想成是 selective hydration 的前身)
image
圖 5 不同渲染模式的特色比較(資料來源:https://www.patterns.dev/vanilla/rendering-patterns)

這幾個渲染方式中,並沒有一個十全十美、效能最優秀的方案可套用於所有情境,該選擇哪種渲染方式需考量應用程式與頁面類型、內容而定,選擇了某方案享有它的優點的同時,也會需要犧牲、包容該方案的某些缺點,需看開發者如何取捨這之中的平衡,就像在 Day 25 渲染模式初探那篇文章所說,每個模式都是為解決特定案例而設計,適合的渲染模式可以為產品展現更出色的效能與表現,不適合的渲染模式則會為充滿創意的應用程式帶來負面影響。

另外補充,在 Patterns for Building JavaScript Websites in 2022 這篇文章中,作者提供了另一種角度,以常見的前端應用來分析其特徵並提出可能適合的渲染策略,對於不知如何選擇渲染模式的開發者來說,也可作為參考。

表 1 不同前端應用的特徵與適合的渲染策略(資料來源:https://dev.to/this-is-learning/patterns-for-building-javascript-websites-in-2022-5a93)

Portfolio Content Storefront Social Network Immersive
Holotype Personal Blog CNN Amazon Facebook Figma
Interactivity Minimal Linked Articles Purchase Multi-Point, Real-time Everything
Session Depth Shallow Shallow Shallow - Medium Extended Deep
Values Simplicity Discover-ability Load Performance Dynamicism Immersiveness
Routing Server Server, Hybrid Hybrid, Transitional Transitional, Client Client
Rendering Static Static, SSR Static, SSR SSR CSR
Hydration None Progressive, Partial Partial, Resumable Any None (CSR)
Example Framework 11ty Astro, Elder Marko, Qwik, Hydrogen Next, Remix Create React App**

** Create React App:React 官方文件中已不再提及 Create React App,如果是作為單純練習用還可使用 Create React App,如果想開發 React CSR 的應用,個人建議可改用 Vite。

Reference


上一篇
[Day 28] Static Rendering/Static Site Generation (SSG)
下一篇
[Day 30] 系列文總結與完賽心得
系列文
30天的 JavaScript 設計模式之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言